Skip to content

feat(sidebar-v2): thread snoozing#4311

Merged
t3dotgg merged 15 commits into
mainfrom
t3/sidebar-v2-snooze
Jul 24, 2026
Merged

feat(sidebar-v2): thread snoozing#4311
t3dotgg merged 15 commits into
mainfrom
t3/sidebar-v2-snooze

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Adds an inbox-zero snooze lifecycle to Sidebar v2: hide a thread from the inbox until a chosen time, then bring it back loudly. Design follows the approved plan (option B popover entry + option C collapsed shelf + option F context-menu parity, "Woke" pill for returns): https://2udjpqoxpjoz.postplan.dev

Snooze is an overlay on the active state, not a fourth destination — crisply distinct from Settle ("done, tuck below") and Archive ("out of the sidebar"): snooze is "not now, guaranteed to come back."

image image image

Contracts + server

  • New thread.snooze / thread.unsnooze commands and thread.snoozed / thread.unsnoozed events; snoozedUntil / snoozedAt on OrchestrationThread + shell (optional fields, so payloads from pre-snooze servers still decode)
  • Decider invariants: wake time must be in the future; blocked-on-you work (open approval/user-input request) cannot be snoozed; duplicate snoozes re-emit idempotently (same pattern as settle); a user message on a snoozed thread spends the return ticket (thread.unsnoozed, reason activity)
  • Migration 034 adds snoozed_until / snoozed_at to projection_threads; projector, projection pipeline, and every snapshot/shell query thread the fields through
  • New threadSnooze capability flag, version-skew gated exactly like threadSettlement — clients never send the commands to older servers

Shared logic (client-runtime)

  • effectiveSnoozed: hidden while the wake time is ahead and the thread hasn't raised its hand. Timer wakes are derived — no server event fires; a passed snoozedUntil simply stops classifying as snoozed
  • threadRaisedHandWhileSnoozed: pending approval/input, a failure newer than the snooze, or a run completed after the snooze wake the thread early (classification only — the snooze fields stay put, mirroring how effectiveSettled treats blocked work). Snoozing a thread that already failed stays snoozed: that snooze was "I saw it, not now"
  • threadWokeAt feeds the woke indicator until the user visits
  • Snooze never pauses the agent — a working thread keeps working; only visibility changes

Sidebar v2 UI

  • Entry: hover clock button beside Settle opens a preset popover — In 1 hour / This evening / Tomorrow 9:00 / Next week — with "Until PR merges" / "Until next review" visible as SOON teasers (the event-based schema hook lands next)
  • Destination: collapsed "Snoozed · N" shelf between Active and Settled — snoozed threads are never gone without a trace. Expanded rows show wake countdowns (2h, 18h) and a hover wake-now button; the shelf disappears entirely at count 0
  • Wake: amber Woke pill (alarm-clock icon) + unread-weight title, in the thread's original position — the calm static sort is untouched; the pill carries the signal. Clears on visit
  • Parity: context-menu Snooze › submenu on single rows and Snooze (N) on multi-select (only when every selected thread can take it); undo toast after every snooze

Tests

  • decider.snoozed.test.ts — invariants, idempotent re-emission, activity wake on turn.start
  • threadSnoozed.test.ts — effectiveSnoozed / raised-hand / canSnooze / threadWokeAt edge cases (incl. pre-snooze failures staying snoozed)
  • Sidebar.snooze.test.ts — preset boundaries (evening suppression, Monday next-week), wake labels
  • Existing settled/projection fixtures extended with the new columns

pnpm typecheck, pnpm lint (no new warnings), and full test suites for server (1571 passed), web (1444), client-runtime (467), contracts (187) all green. Codex review ran on the diff; its one finding (pre-existing failures instantly un-hiding a fresh snooze) is fixed and covered by a test.

🤖 Generated with Claude Code


Note

Medium Risk
Touches orchestration commands, projection schema, and list partitioning on web and mobile; invariants mirror settle but new lifecycle paths (activity wake, raised-hand classification) add behavioral surface area.

Overview
Adds thread snoozing end-to-end: hide threads until a future wake time, with visibility and inbox behavior aligned to settlement patterns but without pausing the agent.

Server & persistence: New thread.snooze / thread.unsnooze commands and matching events; snoozedUntil / snoozedAt on threads via migration 034 and projection plumbing. The decider enforces future wake times, blocks snooze when there are open approvals/user-input or a queued turn start, supports idempotent re-snooze, and unsnoozes on user message (activity). threadSnooze capability is advertised like threadSettlement.

Shared client logic: effectiveSnoozed, canSnooze, threadRaisedHandWhileSnoozed, and threadWokeAt — timer wakes are client-derived (no wake event); blocked work, new failures, or post-snooze completions can surface threads early without clearing snooze fields.

Web Sidebar v2: Preset snooze popover and context menus, collapsible Snoozed shelf, wake countdowns, Woke pill until visit, composer banner for snoozed active thread with Wake now, and forward navigation when snoozing the open thread (shared with settle).

Mobile: Thread list v2 hides snoozed threads (capability-gated), arms setTimeout on nextSnoozeWakeAt for second-precise re-partition, and improves empty states when the inbox is all-snoozed.

Reviewed by Cursor Bugbot for commit efdf69b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add thread snoozing to sidebar with preset durations, wake indicators, and server persistence

  • Adds end-to-end thread snoozing across web and mobile: threads can be snoozed via preset durations (1 hour, this evening, tomorrow, next week) from row hover actions and context menus, with an Undo toast on success.
  • Snoozed threads are hidden into a collapsible "Snoozed" shelf in the web sidebar (sorted by earliest wake) and excluded from the mobile thread list; both surfaces re-partition precisely at wake time via a scheduled setTimeout.
  • Threads that wake early (due to pending approvals, user input, or session activity) surface a "Woke" status pill until visited; the chat view composer shows a banner for snoozed threads with an immediate-wake button.
  • New thread.snooze/thread.unsnooze commands are added to the orchestration protocol, with server-side validation (future wake time, no blocking requests), idempotent event emission, and automatic unsnooze on user message.
  • A database migration (034_ProjectionThreadsSnoozed.ts) adds snoozed_until and snoozed_at columns to projection_threads; the threadSnooze capability flag gates the feature per environment.

Macroscope summarized efdf69b.

Summary by CodeRabbit

  • New Features
    • Added thread snoozing with presets for one hour, this evening, tomorrow, or next week.
    • Snoozed threads appear in a dedicated, collapsible sidebar section and automatically return when their wake time arrives.
    • Added options to snooze or unsnooze individual and multiple threads.
    • Threads can wake early when new activity, approvals, input requests, or failures require attention.
  • Bug Fixes
    • Prevented snoozing threads with blocked or queued work.
    • Improved navigation to skip snoozed threads after settling.

Adds an inbox-zero snooze lifecycle to Sidebar v2: hide a thread until a
chosen time, then bring it back loudly.

Contracts + server:
- thread.snooze / thread.unsnooze commands and thread.snoozed /
  thread.unsnoozed events; snoozedUntil/snoozedAt on thread + shell
  (optional, so pre-snooze payloads still decode)
- decider invariants: wake time must be in the future; blocked-on-you
  work (open approval/user-input) cannot be snoozed; duplicates re-emit
  idempotently; a user message on a snoozed thread spends the return
  ticket (thread.unsnoozed reason "activity")
- migration 034 adds snoozed_until/snoozed_at to projection_threads;
  projector, projection pipeline, and all snapshot/shell queries thread
  the fields through
- threadSnooze capability flag, version-skew gated like threadSettlement

Shared logic (client-runtime):
- effectiveSnoozed: hidden while the wake time is ahead AND the thread
  hasn't raised its hand; timer wakes are derived (no server event)
- threadRaisedHandWhileSnoozed: pending approval/input, a failure newer
  than the snooze, or a run completed after the snooze wake early —
  classification only, without clearing the server-side snooze
- threadWokeAt drives the woke indicator until the user visits

Sidebar v2 UI:
- hover clock button beside Settle opening a preset popover (In 1 hour /
  This evening / Tomorrow / Next week; event-based options teased as SOON)
- collapsed "Snoozed · N" shelf between Active and Settled — never hides
  threads without a trace; expanded rows show wake countdowns and a
  wake-now button; shelf disappears at count 0
- amber "Woke" pill + unread-weight title when a snooze ends; the static
  sort is untouched, the pill carries the signal
- context-menu Snooze submenu (single + multi-select), undo toast

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding thread snoozing to Sidebar v2.
Description check ✅ Passed The description covers what changed, why, UI screenshots, and test status, though it does not follow the exact template headings or include a checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3/sidebar-v2-snooze

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 22, 2026
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Comment thread apps/server/src/orchestration/decider.ts
Comment thread apps/server/src/orchestration/decider.ts
Comment thread packages/client-runtime/src/state/threadSettled.ts
Comment thread apps/web/src/components/Sidebar.snooze.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a significant new feature (thread snoozing) with database migrations, new server-side commands/events, and substantial client-side UI changes including new components and state management. New features of this scope warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Merge resolves the SidebarV2 restyle (#4268) by re-applying snooze onto
the new surface model (sidebar tokens, thread tooltips, restyled action
buttons), and folds in serverSelfUpdate capability alongside threadSnooze.

Review fixes:
- Snooze classification now uses a precise clock instead of the
  minute-quantized partition time, plus a timeout armed at the earliest
  upcoming wake — the shelf empties the moment a snooze expires instead
  of up to a minute late (Cursor)
- thread.snooze now rejects queued turn starts exactly like
  thread.settle, server-side (shared threadHasQueuedTurnStart helper)
  and client-side (canSnooze gains the hasQueuedTurnStart check) (Cursor)
- threadWokeAt keeps an early hand-raise wake authoritative after the
  scheduled wake time passes, so a visited-and-cleared Woke pill cannot
  resurface when snoozedUntil elapses (Macroscope)
- resolveSnoozePresets advances calendar days with setDate instead of
  DAY_MS offsets, fixing tomorrow/next-week presets across DST
  transitions (Macroscope)
- Documented why hasOpenBlockingRequest's scan of the 500-capped
  activities array is sound for the snooze invariant (Macroscope)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Updated the branch (merged main, incl. the #4268 sidebar restyle — snooze UI re-applied onto the new surface tokens) and addressed all four review findings:

  • Mismatched clocks (Cursor): snooze classification now reads a precise clock instead of the minute-quantized partition time, and a timeout is armed at the earliest upcoming wake — the shelf empties the moment a snooze expires, and wake labels can't show "now" while still hidden.
  • Queued turns (Cursor): thread.snooze now rejects queued turn starts with the same detection and clock-skew bounds as thread.settle (extracted into a shared threadHasQueuedTurnStart helper); canSnooze mirrors it client-side via hasQueuedTurnStart. Covered by new decider + shared-logic tests.
  • threadWokeAt resurfacing a cleared pill (Macroscope): early hand-raise wakes now stay authoritative after the scheduled wake time passes — the hand-raise branch is checked before the timer branch, with a regression test.
  • DST preset bug (Macroscope): resolveSnoozePresets advances calendar days with setDate instead of DAY_MS offsets.
  • Capped-activities scan (Macroscope): documented on hasOpenBlockingRequest why the 500-entry cap is sound — an open request blocks its turn, so a request can't scroll out of the window while still open; the projection's pendingApprovalCount reads the same stream and stays consistent.

All suites green: server 1597, web 1450, client-runtime 469, contracts 187; repo-wide typecheck + lint clean.

🤖 Generated with Claude Code

Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Merge folds the snooze thread tooltip into main's reworked
SidebarV2ThreadTooltip (glass styling, branchMismatch row, no status
prop).

Review fix (Cursor): isWoke, snoozeWakeLabel, and snoozeWakeDescription
now parse ISO timestamps through the shared parseTimestampDate utility
(newly exported) instead of raw Date.parse/new Date, so an unparseable
lastVisitedAt counts as never-visited rather than eating the Woke pill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Branch updated again — merged latest main (the tooltip rework landed there, so the snooze tooltip now rides SidebarV2ThreadTooltip's new glass styling and branchMismatch row) and addressed the remaining Bugbot finding:

  • parseTimestampDate consistency (Cursor): isWoke, snoozeWakeLabel, and snoozeWakeDescription now parse ISO timestamps through the shared parseTimestampDate utility (exported from timestampFormat.ts) instead of raw Date.parse / new Date. An unparseable lastVisitedAt now counts as never-visited, so corrupt local visit data can't suppress the Woke pill.

Typecheck, lint, and the web suite (1471 tests) are green; PR is mergeable again.

🤖 Generated with Claude Code

Comment thread apps/web/src/components/SidebarV2.tsx Outdated
- threadWokeAt: drop the redundant effectiveSnoozed call and re-parse —
  one classification pass; the tail collapses to a single timer-elapsed
  expression
- SnoozePopoverButton: fully controlled by the row's existing
  snoozeMenuOpen state instead of mirroring it in a child useState
- Multi-select snooze: drop the snoozableThreads.length === count guard
  (threadKeys is already filtered against the same map with no
  intervening mutation)
- decider thread.snooze: hoist existingSnoozedAt so the payload doesn't
  re-check thread.snoozedAt for narrowing
- Un-export SnoozePresetId (no external consumers)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg t3dotgg changed the title feat(sidebar-v2): thread snooze with wake-loud return feat(sidebar-v2): thread snoozing Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/server/src/orchestration/decider.snoozed.test.ts (1)

146-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guarded payload assertions can pass vacuously.

The payload checks only execute inside if (events[0]?.type === "thread.snoozed"); if the decider ever stopped emitting thread.snoozed (or emitted nothing), this test would still pass silently. Add an unconditional expect(events[0]?.type).toBe("thread.snoozed") (and a length assertion here) so the narrowing guard becomes redundant rather than load-bearing. The idempotent-duplicate test at Lines 137-142 has the same pattern.

💚 Proposed assertion hardening
       const events = Array.isArray(event) ? event : [event];
+      expect(events).toHaveLength(1);
+      expect(events[0]?.type).toBe("thread.snoozed");
       if (events[0]?.type === "thread.snoozed") {
         expect(events[0].payload.snoozedUntil).toBe("1970-01-03T09:00:00.000Z");
         expect(events[0].payload.updatedAt).not.toBe(NOW);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/decider.snoozed.test.ts` around lines 146 -
163, Harden the assertions in the “re-snoozing to a DIFFERENT wake time stamps
fresh” test and the idempotent-duplicate test by asserting the expected event
array length and unconditionally asserting events[0]?.type is "thread.snoozed"
before checking payload fields. Remove the conditional type guards so payload
assertions cannot pass vacuously, while preserving the existing expected payload
checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/SidebarV2.tsx`:
- Around line 1178-1187: The snooze wake timer in the useEffect must cap delayMs
at the maximum 32-bit setTimeout delay of 2,147,483,647 ms. Preserve the
existing non-negative calculation and cleanup behavior while clamping far-future
snoozedUntil values before calling window.setTimeout.

---

Nitpick comments:
In `@apps/server/src/orchestration/decider.snoozed.test.ts`:
- Around line 146-163: Harden the assertions in the “re-snoozing to a DIFFERENT
wake time stamps fresh” test and the idempotent-duplicate test by asserting the
expected event array length and unconditionally asserting events[0]?.type is
"thread.snoozed" before checking payload fields. Remove the conditional type
guards so payload assertions cannot pass vacuously, while preserving the
existing expected payload checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 272338c3-d45e-42a8-9022-197757f034bb

📥 Commits

Reviewing files that changed from the base of the PR and between b44ed83 and fe4bd6d.

📒 Files selected for processing (29)
  • apps/mobile/src/state/use-thread-selection.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
  • apps/server/src/orchestration/Schemas.ts
  • apps/server/src/orchestration/decider.snoozed.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/orchestration/projector.test.ts
  • apps/server/src/orchestration/projector.ts
  • apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
  • apps/server/src/persistence/Layers/ProjectionThreads.ts
  • apps/server/src/persistence/Migrations.ts
  • apps/server/src/persistence/Migrations/034_ProjectionThreadsSnoozed.ts
  • apps/server/src/persistence/Services/ProjectionThreads.ts
  • apps/web/src/components/Sidebar.snooze.test.ts
  • apps/web/src/components/Sidebar.snooze.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/hooks/useThreadActions.ts
  • apps/web/src/state/entities.ts
  • apps/web/src/timestampFormat.ts
  • packages/client-runtime/src/operations/commands.ts
  • packages/client-runtime/src/state/threadCommands.ts
  • packages/client-runtime/src/state/threadDetail.ts
  • packages/client-runtime/src/state/threadReducer.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/client-runtime/src/state/threadSnoozed.test.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts

Comment on lines +1178 to +1187
useEffect(() => {
const nextWakeAtMs =
snoozedThreads.length > 0 && snoozedThreads[0]?.snoozedUntil != null
? Date.parse(snoozedThreads[0].snoozedUntil)
: Number.NaN;
if (Number.isNaN(nextWakeAtMs)) return;
const delayMs = Math.max(0, nextWakeAtMs - Date.now()) + 50;
const id = window.setTimeout(() => bumpSnoozeWakeTick((tick) => tick + 1), delayMs);
return () => window.clearTimeout(id);
}, [snoozedThreads]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any max-snooze / horizon validation on snoozedUntil in the decider/contracts.
rg -nP 'snoozedUntil' --type=ts -C3 apps/server/src/orchestration packages/contracts/src | rg -nP -i 'max|horizon|clamp|limit|valid|Date\.parse|throw' -C2

Repository: pingdotgg/t3code

Length of output: 687


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the snooze wake effect plus the snoozed thread ordering.
ast-grep outline apps/web/src/components/SidebarV2.tsx --view expanded | sed -n '1,220p'

printf '\n--- effect slice ---\n'
sed -n '1160,1205p' apps/web/src/components/SidebarV2.tsx

printf '\n--- snoozedThreads references ---\n'
rg -n "snoozedThreads" apps/web/src/components/SidebarV2.tsx

printf '\n--- server invariant context ---\n'
sed -n '530,560p' apps/server/src/orchestration/decider.ts

Repository: pingdotgg/t3code

Length of output: 4611


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the browser timer coercion behavior with a tiny read-only probe.
node - <<'JS'
const vals = [2_147_483_647, 2_147_483_648, 3_000_000_000, 10_000_000_000, Number.MAX_SAFE_INTEGER];
for (const v of vals) {
  const n = v | 0;
  console.log(JSON.stringify({
    input: v,
    int32: n,
    unsigned: n >>> 0,
    signed32: (n >> 0),
  }));
}
JS

Repository: pingdotgg/t3code

Length of output: 569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the timer coercion and 32-bit boundary directly in JS semantics.
node - <<'JS'
const delays = [0, 1, 1000, 2_147_483_647, 2_147_483_648, 3_000_000_000];
for (const d of delays) {
  const signed = d >> 0;
  console.log(`${d} => signed32=${signed}, maxed=${Math.max(0, d)} `);
}
JS

Repository: pingdotgg/t3code

Length of output: 402


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find existing timeout clamps or timer utilities in the web app.
rg -n "2147483647|max.*timeout|setTimeout\\(|clearTimeout\\(" apps/web/src | sed -n '1,220p'

Repository: pingdotgg/t3code

Length of output: 9145


🌐 Web query:

window.setTimeout 2147483647 clamp browser overflow immediate fire specification

💡 Result:

The setTimeout delay parameter is limited to a maximum value of 2,147,483,647 milliseconds (the maximum value for a signed 32-bit integer) [1][2]. This limitation exists because browsers and environments like Node.js historically convert the provided delay value into a signed 32-bit integer [1][3]. When you provide a value exceeding 2,147,483,647: 1. Integer Overflow: In many browsers, providing a value greater than 2,147,483,647 can lead to integer overflow, where the value wraps around to a negative number [1][4]. Because setTimeout treats negative values as 0 (executing the callback as soon as possible in the next event cycle), the timer effectively triggers immediately [1][3][2]. 2. Environment Variations: While the underlying cause is the 32-bit limitation, behavior can vary slightly by environment [5]. For instance, Node.js explicitly documentation states that when a delay is larger than 2,147,483,647, it is set to 1 millisecond, resulting in immediate execution [6]. 3. Specification Status: Although widely implemented this way due to historical and technical constraints (often rooted in WebIDL 'long' type definitions), the HTML specification does not mandate this specific overflow behavior, nor does it explicitly define a maximum cap [7][5]. However, changing this behavior is generally avoided by implementers to prevent breaking existing legacy code that may rely on or inadvertently trigger this behavior [7][5]. To work around this limitation for delays longer than ~24.8 days, developers commonly use a recursive approach, scheduling multiple smaller, compliant timeouts to eventually trigger the callback at the desired time [3][8].

Citations:


Clamp the snooze wake timeout to 2,147,483,647 ms
setTimeout uses a 32-bit delay, so a far-future snoozedUntil can fire much sooner than intended and keep re-arming this effect. Clamping avoids a tight timer loop for synced values outside the current UI presets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/SidebarV2.tsx` around lines 1178 - 1187, The snooze
wake timer in the useEffect must cap delayMs at the maximum 32-bit setTimeout
delay of 2,147,483,647 ms. Preserve the existing non-negative calculation and
cleanup behavior while clamping far-future snoozedUntil values before calling
window.setTimeout.

Merge picks up project grouping (projectDisplayNameByKey, grouped
pickers) and the borderless hover-action styling; the snooze/settle
hover cluster now uses main's transparent button treatment.

Review fixes:
- Woke pill skipped on slim rows (Cursor): threadWokeAt now feeds every
  section, not just active cards — a thread that wakes straight into the
  settled tail (e.g. PR merged while snoozed) shows an amber Woke marker
  in its time slot and full-strength title until visited
- Clamp the snooze wake timeout to 2,147,483,647 ms (CodeRabbit):
  far-future wakes beyond the signed-32-bit setTimeout range no longer
  overflow into an immediate-fire re-arm loop; the timer re-arms at the
  clamp until the wake is in range

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Branch updated — merged latest main (project grouping + the borderless hover-action styling; the snooze popover button and hover cluster now follow the transparent treatment) and addressed both new findings:

  • Woke pill skipped on slim rows (Bugbot): confirmed real — a thread whose PR merged while snoozed wakes directly into the settled tail, and slim rows always received wokeAt={null}. threadWokeAt now feeds every section; woken slim rows show an amber alarm-clock "Woke" marker in the time slot plus a full-strength title until visited (still-snoozed rows resolve to null on their own, so the shelf is unaffected).
  • 32-bit setTimeout overflow (CodeRabbit): the wake-timer delay is now clamped to 2,147,483,647 ms. A far-future snoozedUntil (e.g. a future event-condition snooze synced from another device) re-arms at the clamp (~24.8 days) instead of overflowing into an immediate-fire loop.

Typecheck + lint clean, web suite green (1479 tests), PR mergeable again.

🤖 Generated with Claude Code

Comment thread apps/server/src/orchestration/decider.ts
Comment thread apps/mobile/src/state/use-thread-selection.ts
Review fixes:
- decider thread.snooze (Cursor): the future-wake invariant now uses a
  negated comparison, so an unparseable snoozedUntil (NaN — IsoDateTime
  is structurally just a string) is rejected instead of persisting
  snooze fields that never hide the thread
- mobile thread list (Cursor): buildThreadListV2Items now classifies
  snoozed threads via effectiveSnoozed (capability-gated per
  environment, snooze outranks settled — same rules as web) and hides
  them, reporting snoozedCount. Same account no longer shows different
  inbox visibility across clients; a shelf UI can consume the count
  later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/mobile/src/features/home/HomeScreen.tsx
…light fade)

Woke rows are exempted from the new in-flight fade — a wake signal must
not recede — and the slim-row time slot keeps the snooze wake countdown /
Woke marker ahead of main's settled-vs-thread time labels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Addressed both new Bugbot findings and re-merged main (twice — #4274's sidebar polish landed mid-update):

  • Invalid snooze wake time accepted (decider): confirmed — IsoDateTime is structurally just a string, and NaN <= x is false, so garbage snoozedUntil slipped past the future-wake invariant and persisted snooze fields that never hide the thread. The invariant now uses the negated comparison (!(parse > now)), which rejects both past and unparseable wake times. Regression test added.
  • Mobile list ignores snooze state: confirmed — mobile shells carried the fields but buildThreadListV2Items never consulted them, so the same account showed different inbox visibility per client. The mobile partition now classifies via effectiveSnoozed (capability-gated per environment via the new snoozeEnvironmentIds, snooze outranking settled — identical rules to web) and hides snoozed threads, returning a snoozedCount a future shelf UI can consume. Both mobile callers (Home list, iPad sidebar) wired; tests cover the hide, the timer wake returning a thread, and the no-capability passthrough.

Merge notes: woke rows are exempted from #4274's new in-flight fade (a wake signal must not recede), and the slim-row time slot keeps the wake countdown / Woke marker ahead of the new settled-vs-thread time labels.

All suites green (server 1611, web 1490, client-runtime 469, mobile 540); typecheck clean; PR mergeable.

🤖 Generated with Claude Code

Comment thread apps/mobile/src/features/threads/threadListV2.ts
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Review fixes:
- SidebarV2 (Macroscope): if a thread becomes blocked while its snooze
  popover is open, the button unmounts without firing onOpenChange and
  snoozeMenuOpen stuck true — permanently hiding the status label and
  pinning the hover actions. The pin is now derived
  (raw && showSnoozeButton) and the raw state resets when the button
  hides, so the popover can't resurrect on remount either.
- mobile thread list (Cursor ×2): snooze classification now takes a
  second-precise snoozeNow alongside the minute-quantized partition
  clock, and the layout reports nextSnoozeWakeAt; both callers arm a
  clamped timeout at that boundary. A woken thread reappears the moment
  its snooze expires instead of up to a minute late — same behavior as
  web.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Addressed all three new findings:

  • Stale snooze-menu pin (Macroscope): confirmed — if a thread became blocked (approval/input) while its snooze popover was open, SnoozePopoverButton unmounted without firing onOpenChange(false), leaving snoozeMenuOpen stuck true: status label permanently hidden, hover actions permanently pinned. The pin is now derived (raw && showSnoozeButton), and an effect clears the raw state when the button hides so the popover can't resurrect on a later remount.
  • Mobile snooze wake minute-quantized (Bugbot ×2, same root cause): confirmed — my mobile parity fix reused the minute-floored partition clock, so a woken thread stayed hidden up to ~59s and snoozedCount lagged. buildThreadListV2Items now takes a second-precise snoozeNow for snooze classification (partition clock unchanged for settle memoization), returns nextSnoozeWakeAt, and both callers (Home list, iPad sidebar) arm a clamped timeout at that boundary — the same fire-at-the-wake-moment behavior as web. Test covers the floored-vs-precise divergence and the reported wake boundary.

Suites green (mobile 541, web 1490, plus server/client-runtime unchanged); typecheck clean; PR mergeable.

🤖 Generated with Claude Code

Comment thread apps/mobile/src/features/home/HomeScreen.tsx Outdated
The wake-timer effect depended only on nextSnoozeWakeAt; when a fire
didn't change the boundary (clamped far-future wake, or a fire landing
just before the wake instant), the chain died and the thread stayed
hidden until the minute tick. snoozeWakeTick joins the deps so every
fire re-arms. (Web is unaffected: its effect depends on the
snoozedThreads array, which gets a fresh identity per recompute.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Fixed the timer re-arm finding: the mobile wake-timer effect now also depends on snoozeWakeTick, so a fire that leaves nextSnoozeWakeAt unchanged (clamped far-future wake, or a fire landing just before the wake instant) re-arms instead of killing the chain. Web is unaffected — its effect keys on the snoozedThreads array, which gets a fresh identity every recompute. Mobile suite green (541), typecheck clean, still mergeable.

🤖 Generated with Claude Code

Menu cleanup per review:
- drop the "Until PR merges" / "Until next review" SOON teasers
- drop the "Snooze until" header — the presets speak for themselves
- presets carry a whenLabel for the time column that complements the
  label instead of repeating it ("Tomorrow · 9:00 AM", not
  "Tomorrow · tomorrow 9:00 AM"); context menus share it. The toast
  keeps the full standalone description.

Merge picks up the warm-thread change-request settle guard
(CHANGE_REQUEST_SETTLE_IDLE_MS) alongside the snooze logic in
threadSettled.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/mobile/src/features/home/HomeScreen.tsx
When every thread in scope is snoozed, the v2 Home list and iPad
sidebar showed "No threads yet" — an all-snoozed inbox read as data
loss. Both empty states now consume the layout's snoozedCount and say
"N threads snoozed" instead (search still outranks it; a real empty
scope is unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Fixed the empty-state finding: when every thread in scope is snoozed, the mobile v2 Home list and iPad sidebar now say "N threads snoozed" (with "Snoozed threads return when their wake time passes" as the detail on Home) instead of the misleading "No threads yet". The layout's previously-unused snoozedCount now drives both; search results and genuinely-empty scopes are unchanged. This is exactly the "never hide without a trace" rule the web shelf implements — mobile now has its minimal equivalent until a real shelf UI lands. Mobile suite green (542), typecheck clean, mergeable.

🤖 Generated with Claude Code

t3dotgg and others added 2 commits July 23, 2026 19:28
Snooze and settle both park the thread you're done with for now, so
they share forward-navigation behavior: snoozing the thread you're
looking at advances to the next remaining card (skipping settled,
snoozed, and same-batch rows), or a fresh draft in the project when it
was the last active one. The settle navigation planner is extracted
into planForwardNavigation and both actions use it; multi-select
snooze passes its batch as coSnoozingKeys the same way bulk settle
does. Failures never navigate, and a user navigation during the await
still wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 00e4e64. Configure here.

Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Comment thread apps/mobile/src/features/home/HomeScreen.tsx
Review fixes:
- web (Cursor): a snoozed thread opened via route (deep link, or open
  while snoozed elsewhere) now keeps its sidebar row under the
  collapsed shelf — same exception the settled tail's "Show more"
  makes — so the active highlight, wake affordance, and threadByKey
  entry survive.
- mobile (Cursor): with an active search query, an empty list whose
  matches are all snoozed now says "All matching threads snoozed"
  (Home list and iPad sidebar) instead of the misleading "No results" —
  those threads passed the same search filter before being counted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Both new findings addressed:

  • Snoozed route thread hidden (web): confirmed — the collapsed shelf dropped every snoozed row, so a snoozed thread opened via deep link (or snoozed from another device while open) lost its sidebar row, highlight, and threadByKey entry. visibleSnoozedThreads now pins the routed thread into the shelf even when collapsed, mirroring the exception the settled tail's "Show more" already makes for the open thread.
  • Search hides snoozed empty state (mobile): confirmed — snoozed threads pass the search filter before being counted, so "No results" was wrong when all matches were snoozed. Both the Home list and iPad sidebar now say "All matching threads snoozed" (or "1 matching thread snoozed") in that case; a genuinely matchless query still reads "No results".

Web 1505 / mobile 542 tests green, typecheck clean, mergeable.

🤖 Generated with Claude Code

@maria-rcks maria-rcks mentioned this pull request Jul 24, 2026
4 tasks
@t3dotgg
t3dotgg merged commit 202e560 into main Jul 24, 2026
16 checks passed
@t3dotgg
t3dotgg deleted the t3/sidebar-v2-snooze branch July 24, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants